//#define ASYNC
// License Notification
//======================================================
// Copyright (c) 2010 Channel Intelligence Inc.
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// which accompanies this distribution, and is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// ======================================================




#if S3SOAP
using System;
using System.IO;
using System.IO.Compression;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using System.Threading;
using System.Text;
using System.Security.Cryptography;
using System.Web.Services.Protocols;
using CIP4.Common;
using CIP4.Item;
using CIP4.Simple;

namespace CIP4.DataProvider.S3
{
    class S3BucketType
    {
        #region Static Features

        private static string AWSAccessKey { get; set; }

        private static string AWSSecretKey { get; set; }

        [ThreadStatic]
        public static Dictionary<Guid, S3BucketType> Buckets;

        public static void Initialize(string accessKey, string secretKey)
        {
            AWSAccessKey = accessKey;
            AWSSecretKey = secretKey;
        }

        public static S3BucketType GetBucket(CollectionType collection, IDataProvider cachingProvider, LoggerWrapperDelegate loggerWrapper, LoggerDelegate logger)
        {
            if (Buckets == null)
                Buckets = new Dictionary<Guid, S3BucketType>();
            if (!Buckets.ContainsKey(collection.ExternalIDGuid()))
                Buckets.Add(collection.ExternalIDGuid(), new S3BucketType(collection, cachingProvider, loggerWrapper));
            return Buckets[collection.ExternalIDGuid()];
        }

        private const int MAXRETRYCOUNT = 3;
        
        private AmazonS3 service;

        #endregion

        #region Public Features & Constructors

        public LoggerWrapperDelegate LoggerWrapper { get; set; }

        public CollectionType BucketCollection { get; set; }

        private S3BucketType(CollectionType collection, IDataProvider cachingProvider, LoggerWrapperDelegate loggerWrapper)
        {
            LoggerWrapper = loggerWrapper;
            service = new AmazonS3();
            service.UnsafeAuthenticatedConnectionSharing = true;
#if ASYNC
            service.GetObjectCompleted += new GetObjectCompletedEventHandler(service_GetObjectCompleted);
#endif
            BucketCollection = collection;
            CreateBucket(BucketCollection.ExternalIDGuid().ToString(), loggerWrapper);

        }

        public List<object> GetItems(IBooleanValue selectionExpression, CursorType cursor)
        {
            List<object> results = new List<object>();
            
            string lookForExternalId = selectionExpression == null? null : selectionExpression.ComparesTo("ExternalID").FirstOrDefault();

            if (!string.IsNullOrEmpty(lookForExternalId))
                GetItem(lookForExternalId, results, selectionExpression, cursor);
            else
            {
                DateTime timeStamp = Aws_GetDatestamp();
                string signature = Aws_GetSignature("ListBucket", timeStamp);
                ListBucketResult listResult = service.ListBucket(BucketCollection.ExternalIDGuid().ToString(), string.Empty, cursor.Marker, cursor.PageSize, true, string.Empty, AWSAccessKey, timeStamp, true, signature, null);

                if (listResult.Contents != null)
                    foreach (ListEntry entry in listResult.Contents)
                    {
                        cursor.Marker = entry.Key;
                        GetItem(entry.Key, results, selectionExpression, cursor);
                    }
            }
            return results;
        }

        public int SaveItems(IEnumerable<object> items, CollectionType collection, ISerializerType serializer, SetHeaderPropertiesDelegate SetHeader)
        {

            MetadataEntry[] metadata = new MetadataEntry[1];
            metadata[0] = new MetadataEntry();
            metadata[0].Name = "Content-Type";
            metadata[0].Value = "text/plain";
            int itemCount = 0;
            foreach (object itemObject in items)
            {
                if (!SetHeader(itemObject, collection, serializer))
                    continue;
                string itemXml = serializer.GetDataForObject(itemObject).ToString();
                byte[] data = Serializer.SerializerBaseType.Compress(itemXml);
                int retry = 0;
                while (retry < MAXRETRYCOUNT)
                {
                    try
                    {
                        DateTime timeStamp = Aws_GetDatestamp();
                        string signature = Aws_GetSignature("PutObjectInline", timeStamp);
                        PutObjectResult s3result = 
                            service.PutObjectInline(
                                BucketCollection.ExternalIDGuid().ToString(), 
                                serializer.GetExternalID(itemObject).ToString() , 
                                metadata, 
                                data, 
                                data.Length, 
                                null, 
                                StorageClass.STANDARD, 
                                true, 
                                AWSAccessKey, 
                                timeStamp, 
                                true, 
                                signature, 
                                null);
                        itemCount++;
                        break;
                    }
                    catch (SoapException ex)
                    {
                        if (ex.Message.EndsWith("Please try again."))
                        {
                            LoggerWrapper(MajorEnum.Error, MinorEnum.S3PleaseTryAgain, "Retry number {0} on item {1}", new object[] { retry, serializer.GetExternalID(itemObject) }, serializer.Logger);
                            retry += 1;
                        }
                        else
                        {
                            throw;
                        }
                    }
                    retry += 1;
                }
                if (retry == MAXRETRYCOUNT)
                {
                    LoggerWrapper(MajorEnum.Error, MinorEnum.RetryCountExceeded, "Retry count exceeded on item {0}", new object[] { serializer.GetExternalID(itemObject) }, serializer.Logger);
                }
            }
            return itemCount;
        }

        public void PurgeAllItems(CollectionType collection, LoggerDelegate logger)
        {
            DateTime timeStamp = Aws_GetDatestamp();
            string signature = Aws_GetSignature("ListBucket", timeStamp);
            ListBucketResult listResult = service.ListBucket(collection.ExternalIDGuid().ToString(), string.Empty, string.Empty, 0, false, string.Empty, AWSAccessKey, timeStamp, true, signature, null);

            if (listResult.Contents != null)
                foreach (ListEntry entry in listResult.Contents)
                {
                    DateTime deleteTimeStamp = Aws_GetDatestamp();
                    string deleteSignature = Aws_GetSignature("DeleteObject", deleteTimeStamp);
                    Status s3result = service.DeleteObject(collection.ExternalIDGuid().ToString(), entry.Key, AWSAccessKey, deleteTimeStamp, true, deleteSignature, null);
                    if (s3result.Code != 204)
                    {
                        LoggerWrapper(MajorEnum.Error, MinorEnum.GenericProvider, "Unexpected S3 error {0} on {1}", new object[] { s3result.Description, entry.Key }, logger);
                        return;
                    }
                    LoggerWrapper(MajorEnum.Trace, MinorEnum.ItemDeleted, "deleted item {0}", new object[] { entry.Key }, logger);
                }
        }

        public int PurgeItems(IEnumerable<ExternalIDType> externalIDs, ISerializerType serializer)
        {
            DateTime timeStamp = Aws_GetDatestamp();
            string signature = Aws_GetSignature("DeleteObject", timeStamp);
            int deletedCount = 0;
            foreach (ExternalIDType externalID in externalIDs)
            {
                int retryCount = 0;
                while (retryCount < MAXRETRYCOUNT)
                {
                    try
                    {
                        Status s3result = service.DeleteObject(BucketCollection.ExternalIDGuid().ToString(), externalID.ToString(), AWSAccessKey, timeStamp, true, signature, null);
                        if (s3result.Code != 204)
                        {
                            LoggerWrapper(MajorEnum.Error, MinorEnum.GenericProvider, "Unexpected S3 error {0} on {1}", new object[] { s3result.Description, externalID }, serializer.Logger);
                            return deletedCount;
                        }
                        deletedCount++;
                        LoggerWrapper(MajorEnum.Trace, MinorEnum.ItemDeleted, 
                            "deleted item {0}", new object[] { externalID }, serializer.Logger);
                        break;
                    }
                    catch (SoapException ex)
                    {
                        if (ex.Message == "We encountered an internal error. Please try again.")
                        {
                            LoggerWrapper(MajorEnum.Error, MinorEnum.S3InternalError, "Retry number {0} on item {1}", new object[] { retryCount, externalID}, serializer.Logger);
                            retryCount += 1;
                        }
                        else
                            throw;
                    }
                }
                if (retryCount == MAXRETRYCOUNT)
                {
                    LoggerWrapper(MajorEnum.Error, MinorEnum.RetryCountExceeded, "Retry count exceeded on item {1}", new object[] { externalID }, serializer.Logger );
                }
            }
            return deletedCount;
        }

        public bool DeleteCollection(CollectionType collection, LoggerDelegate logger)
        {
            DateTime timeStamp = Aws_GetDatestamp();
            string signature = Aws_GetSignature("DeleteBucket", timeStamp);
            PurgeAllItems(collection, logger);
            Status s3Result = service.DeleteBucket(collection.ExternalIDGuid().ToString(), AWSAccessKey, timeStamp, true, signature, null);
            if (s3Result.Code == 204)
            {
                LoggerWrapper(MajorEnum.Trace, MinorEnum.DeleteCollection, "The collection has no content: {0}",
                    new object[] { collection.Name }, logger);
                Buckets.Remove(collection.ExternalIDGuid());
                return true;
            }
            if (s3Result.Code != 500)
            {
                LoggerWrapper(MajorEnum.Error, MinorEnum.ErrorDeletingCollection, "Error deleting collection: {0} code: {1} description: {2} ",
                    new object[] { collection.Name, s3Result.Code.ToString(), s3Result.Description }, logger);
                return false;
            }

            Buckets.Remove(collection.ExternalIDGuid());
            return true;
        }

        #endregion

#region Privates

#if !ASYNC
        private void GetItem(string externalID, List<object> result, IBooleanValue selectionExpression, CursorType cursor)
        {
            int retryCount = 0;
            ISerializerType serializer = cursor.GetSerializer(SerializerKindEnum.CIP4Xml);
            while (retryCount <= 3)
            {
                try
                {
                    DateTime timeStamp = Aws_GetDatestamp();
                    string signature = Aws_GetSignature("GetObject", timeStamp);
                    GetObjectResult getResult = service.GetObject(BucketCollection.ExternalIDGuid().ToString(), externalID, false, true, true, AWSAccessKey, timeStamp, true, signature, null);

                    string text = Serializer.SerializerBaseType.DeCompress(getResult.Data);
                    object value = serializer.GetObjectForData(XElement.Parse(text));

                    if (Simple.DataProvider.CompareProperties(selectionExpression, value, serializer))
                        result.Add(value);
                    return;
                }
                catch (SoapException ex)
                {
                    if (ex.Message.EndsWith("Please try again."))
                        retryCount += 1;
                    else if (ex.Message.EndsWith("The specified key does not exist."))
                        return;
                    else
                        throw;
                }
            }
            throw new ApplicationException(string.Format("Retry count exceeded getting {0}", externalID));
        }
#else
        private AutoResetEvent globalGetEvent;

        private object globalResult;

        private IBooleanValue globalTemplate;

        private ISerializerType globalSerializer;

        private bool GetItem(ExternalIDType externalID, List<object> result, IBooleanValue selectionExpression, CursorType cursor)
        {
            globalSerializer = cursor.GetSerializer(SerializerKindEnum.CIP4Xml);
            globalGetEvent = new AutoResetEvent(false);
            globalResult = null;
            globalTemplate = selectionExpression;
            Thread newThread = new Thread(new ParameterizedThreadStart(GetObjectAsyncThing));
            try
            {
                newThread.Start(externalID);
                if (globalGetEvent.WaitOne(new TimeSpan(0, 0, 5), false))
                {
                    service.CancelAsync(null);
                    result.Add(globalResult);
                    return true;
                }
            }
            finally
            {
                if (newThread != null && !newThread.Join(5))
                    newThread.Abort();
            }
            throw new ApplicationException(string.Format("Wait period exceeded getting {0}", externalID));
        }

        private void GetObjectAsyncThing(object externalIdObject)
        {
            try
            {
                string externalID = externalIdObject.ToString();
                DateTime timeStamp = Aws_GetDatestamp();
                string signature = Aws_GetSignature("GetObject", timeStamp);
                service.GetObjectAsync(
                    BucketCollection.ExternalIDGuid().ToString(), externalID, false, true, true, AWSAccessKey, timeStamp, true, signature, null, externalID);
            }
            catch (SoapException ex)
            {
                if (ex.Message.EndsWith("The specified key does not exist."))
                    return;
                else
                    throw;
            }
        }

        private void service_GetObjectCompleted(object sender, GetObjectCompletedEventArgs e)
        {
            string text = Serializer.SerializerBaseType.DeCompress(e.Result.Data);
            //Console.WriteLine(string.Format("Compressed Deck Size {0}, uncompressed {1}", e.Result.Data.Length, text.Length));
            object value = globalSerializer.GetObjectForData(XElement.Parse(text));

            if (DataProvider.CompareProperties(globalTemplate, value, globalSerializer))
                 globalResult = value;
            globalGetEvent.Set();
        }
#endif
#region S3 Helper Methods

        private const string csISODateFormat = "yyyy-MM-ddTHH:mm:ss.fffZ";

        private const string csAmazonS3 = "AmazonS3";

        //Get a Datetime that can be used with the Amazon Web Services methods
        private DateTime Aws_GetDatestamp()
        {
            DateTime now;
            DateTime dateStamp;
            now = DateTime.Now;
            dateStamp = new DateTime(now.Year,
                now.Month, now.Day,
                now.Hour, now.Minute,
                now.Second,
                now.Millisecond, DateTimeKind.Local);
            return dateStamp;
        }

        //Get a timeStamp string in the correct format required by Amazon Web Services
        private string Aws_GetISOtimeStamp(DateTime timeStamp)
        {
            string isoTimeStamp;
            isoTimeStamp = timeStamp.ToUniversalTime().ToString(csISODateFormat, System.Globalization.CultureInfo.InvariantCulture);
            return isoTimeStamp;
        }

        //Create signature used for Amazon Web Service method calls
        private string Aws_GetSignature(string operation, DateTime timeStamp)
        {
            string raw;
            string signature;
            UTF8Encoding encoding;
            HMACSHA1 objHMACSHA1;
            raw = csAmazonS3 + operation + Aws_GetISOtimeStamp(timeStamp);
            encoding = new UTF8Encoding();
            objHMACSHA1 = new HMACSHA1(encoding.GetBytes(AWSSecretKey));
            signature = Convert.ToBase64String(objHMACSHA1.ComputeHash(encoding.GetBytes(raw.ToCharArray())));
            return signature;
        }

        public void CreateBucket(string bucketName, LoggerWrapperDelegate loggerWrapper)
        {
            DateTime timeStamp;
            string sSignature;
            timeStamp = Aws_GetDatestamp();
            sSignature = Aws_GetSignature("CreateBucket", timeStamp);
            int retryCount = 0;
            while (retryCount <= 3)
            {
                try
                {
                    service.CreateBucket(bucketName, null, AWSAccessKey, timeStamp, true, sSignature);
                    break;
                }
                catch (SoapException ex)
                {
                    if (ex.Message.EndsWith("Please try again."))
                        Thread.Sleep((int)(Math.Pow(10,retryCount++)));
                    else
                        throw;
                }
            }
        }

#endregion
#endregion
    }
}
#endif